<?php

namespace app\common\service;

use Exception;
use app\common\model\file\FileModel;
use think\helper\Str;
use think\facade\Config;
// 引入腾讯云 COS SDK
use Qcloud\Cos\Client;

/**
 * 腾讯云COS上传服务类
 * 支持简单上传和分片上传大文件功能
 */
class CosUploadService
{
    /**
     * COS客户端实例
     * @var Client
     */
    protected $cosClient;
    
    /**
     * 配置参数
     * @var array
     */
    protected $config;
    
    /**
     * 初始化
     * @param array $config COS配置参数
     */
    public function __construct(array $config = [])
    {
        // 合并默认配置和用户配置
        $this->config = array_merge([
            'region' => Config::get('admin.cos_region', 'ap-guangzhou'),
            'credentials' => [
                'secretId' => Config::get('admin.cos_secret_id', ''),
                'secretKey' => Config::get('admin.cos_secret_key', ''),
            ],
            'bucket' => Config::get('admin.cos_bucket', ''),
//            'cdnDomain' => Config::get('admin.cos_cdn_domain', ''),
            'timeout' => 60,
            'connect_timeout' => 30,
            'chunk_size' => 5242880, // 默认5MB分片
            'max_thread' => 10, // 最大并发数
            'sign_expire' => 3600, // 签名过期时间
        ], $config);
        
        // 检查必要配置
        $this->checkConfig();
        
        // 初始化COS客户端
        $this->cosClient = new Client($this->config);
    }
    
    /**
     * 测试COS连接
     * @return bool 是否连接成功
     * @throws Exception
     */
    public function testConnection()
    {
        try {
            // 先验证配置
            $this->checkConfig();
            
            // 打印配置信息（不包含密钥）用于调试
            $debugInfo = [
                'region' => $this->config['region'],
                'bucket' => $this->config['bucket'],
                'secret_id_prefix' => substr($this->config['credentials']['secretId'], 0, 8) . '...'
            ];
            
            // 尝试列出存储桶内容（只读操作，不会修改数据）
            $this->cosClient->ListObjects([
                'Bucket' => $this->config['bucket'],
                'MaxKeys' => 1
            ]);
            return true;
        } catch (Exception $e) {
            // 获取原始错误信息，可能包含详细的权限错误
            $errorMsg = $e->getMessage();
            
            // 分析常见的Access Denied错误原因
            if (strpos($errorMsg, 'Access Denied') !== false) {
                throw new Exception('COS连接失败: Access Denied。请检查：1. SecretId和SecretKey是否正确 2. 该密钥是否有访问此存储桶的权限 3. 存储桶名称格式是否正确 4. 存储桶地域是否设置正确。原始错误：' . $errorMsg);
            } else {
                throw new Exception('COS连接失败: ' . $errorMsg);
            }
        }
    }
    
    /**
     * 检查配置有效性
     * @throws Exception
     */
    protected function checkConfig()
    {
        if (empty($this->config['credentials']['secretId']) || 
            empty($this->config['credentials']['secretKey']) || 
            empty($this->config['bucket']) || 
            empty($this->config['region'])) {
            throw new Exception('腾讯云COS配置不完整');
        }
    }
    
    /**
     * 简单文件上传
     * @param string $filePath 本地文件路径
     * @param string $key 上传到COS的对象键
     * @param array $options 额外选项
     * @return array 上传结果
     * @throws Exception
     */
    public function upload($filePath, $key = null, array $options = [])
    {
        error_log('开始上传文件: ' . basename($filePath));
        
        // 检查文件是否存在
        if (!file_exists($filePath)) {
            throw new Exception('文件不存在');
        }
        
        // 获取文件大小
        $fileSize = filesize($filePath);
        error_log('文件大小: ' . $fileSize . ' bytes');
        
        // 检查文件大小是否超过限制
        $maxSize = Config::get('admin.upload_max_size', 52428800); // 默认50MB
        if ($fileSize > $maxSize) {
            throw new Exception('文件大小超过限制');
        }
        
        // 生成默认key
        if (is_null($key)) {
            $key = $this->generateKey($filePath);
        }
        error_log('上传key: ' . $key);

        try {
            // 设置默认Content-Type
            if (!isset($options['ContentType'])) {
                $options['ContentType'] = $this->getContentType($filePath);
            }

            error_log('执行上传到存储桶: ' . $this->config['bucket'] . ', 地域: ' . $this->config['region']);
            
            // 执行上传
            $result = $this->cosClient->Upload(
                $this->config['bucket'],  // 存储桶名称
                $key,                     // 对象键（文件标识）
                fopen($filePath, 'rb'),   // 文件资源句柄
                $options                  // 上传配置选项
            );
            
            $formattedResult = $this->formatResult($result, $key);
            error_log('上传成功! 文件URL: ' . $formattedResult['url']);
            return $formattedResult;
        } catch (Exception $e) {
            $errorMsg = $e->getMessage();
            error_log('上传失败: ' . $errorMsg);
            
            // 分析常见的错误类型
            if (strpos($errorMsg, 'Access Denied') !== false) {
                // 详细的Access Denied错误诊断
                $diagnosis = [];
                
                // 检查存储桶名称格式
                if (strpos($this->config['bucket'], '-') === false || !preg_match('/-[0-9]{10}$/', $this->config['bucket'])) {
                    $diagnosis[] = '存储桶名称格式错误（必须包含appid，格式为bucket-name-1234567890）';
                }
                
                // 检查密钥是否为空
                if (empty($this->config['credentials']['secretId']) || empty($this->config['credentials']['secretKey'])) {
                    $diagnosis[] = 'SecretId或SecretKey为空';
                }
                
                $diagnosisStr = empty($diagnosis) ? '' : ' 诊断发现: ' . implode(', ', $diagnosis);
                
                throw new Exception('上传失败: Access Denied' . $diagnosisStr . '。请检查：1. SecretId和SecretKey是否正确 2. 该密钥是否有写入此存储桶的权限（请在腾讯云控制台为该密钥配置COS全权限或特定前缀的写入权限） 3. 存储桶名称格式是否正确（必须包含appid） 4. 存储桶地域是否设置正确。原始错误：' . $errorMsg);
            } elseif (strpos($errorMsg, 'Bucket not found') !== false) {
                throw new Exception('上传失败: 存储桶不存在。请检查存储桶名称和地域配置是否正确。原始错误：' . $errorMsg);
            } else {
                throw new Exception('上传失败: ' . $errorMsg);
            }
        }
    }
    
    /**
     * 分片上传大文件
     * @param string $filePath 本地文件路径
     * @param string $key 上传到COS的对象键
     * @param array $options 额外选项
     * @return array 上传结果
     * @throws Exception
     */
    public function uploadSlice($filePath, $key = null, array $options = [])
    {
        error_log('开始分片上传文件: ' . basename($filePath));
        
        // 检查文件是否存在
        if (!file_exists($filePath)) {
            throw new Exception('文件不存在');
        }
        
        // 获取文件大小
        $fileSize = filesize($filePath);
        error_log('文件大小: ' . $fileSize . ' bytes');
        
        // 生成默认key
        if (is_null($key)) {
            $key = $this->generateKey($filePath);
        }
        error_log('上传key: ' . $key);
        
        // 获取进度回调函数
        $progressCallback = isset($options['progress_callback']) ? $options['progress_callback'] : null;
        
        try {
            // 设置默认Content-Type
            $contentType = $this->getContentType($filePath);
            
            error_log('初始化分片上传到存储桶: ' . $this->config['bucket'] . ', 地域: ' . $this->config['region']);
            
            // 初始化分片上传
            $uploadId = $this->initiateMultipartUpload($key, $contentType);
            error_log('分片上传ID: ' . $uploadId);
            
            // 计算分片数
            $chunkSize = $this->config['chunk_size'];
            $chunkCount = ceil($fileSize / $chunkSize);
            error_log('分片片数: ' . $chunkCount);
            $uploadParts = [];
            
            // 执行分片上传
            for ($i = 0; $i < $chunkCount; $i++) {
                $offset = $i * $chunkSize;
                $partSize = min($chunkSize, $fileSize - $offset);
                $partNumber = $i + 1;
                
                error_log('上传分片 ' . $partNumber . '/' . $chunkCount);
                
                // 上传单个分片
                $partResult = $this->uploadPart($filePath, $key, $uploadId, $partNumber, $offset, $partSize);
                
                $uploadParts[] = [
                    'PartNumber' => $partNumber,
                    'ETag' => $partResult['ETag']
                ];
                
                // 调用进度回调
                if ($progressCallback) {
                    call_user_func($progressCallback, $partNumber, $chunkCount);
                }
            }
            
            // 完成分片上传
            error_log('完成分片上传');
            $result = $this->completeMultipartUpload($key, $uploadId, $uploadParts);
            
            $formattedResult = $this->formatResult($result, $key);
            error_log('分片上传成功! 文件URL: ' . $formattedResult['url']);
            return $formattedResult;
        } catch (Exception $e) {
            // 如果有uploadId，尝试终止分片上传
            if (isset($uploadId)) {
                try {
                    $this->abortMultipartUpload($key, $uploadId);
                } catch (Exception $e2) {
                    // 忽略终止上传的错误
                }
            }
            
            $errorMsg = $e->getMessage();
            error_log('分片上传失败: ' . $errorMsg);
            
            // 分析常见的错误类型
            if (strpos($errorMsg, 'Access Denied') !== false) {
                // 详细的Access Denied错误诊断
                $diagnosis = [];
                
                // 检查存储桶名称格式
                if (strpos($this->config['bucket'], '-') === false || !preg_match('/-[0-9]{10}$/', $this->config['bucket'])) {
                    $diagnosis[] = '存储桶名称格式错误（必须包含appid）';
                }
                
                // 检查密钥是否为空
                if (empty($this->config['credentials']['secretId']) || empty($this->config['credentials']['secretKey'])) {
                    $diagnosis[] = 'SecretId或SecretKey为空';
                }
                
                $diagnosisStr = empty($diagnosis) ? '' : ' 诊断发现: ' . implode(', ', $diagnosis);
                
                throw new Exception('分片上传失败: Access Denied' . $diagnosisStr . '。请检查：1. SecretId和SecretKey是否正确 2. 该密钥是否有写入此存储桶的权限（请在腾讯云控制台为该密钥配置COS全权限或特定前缀的写入权限） 3. 存储桶名称格式是否正确（必须包含appid） 4. 存储桶地域是否设置正确。原始错误：' . $errorMsg);
            } elseif (strpos($errorMsg, 'Bucket not found') !== false) {
                throw new Exception('分片上传失败: 存储桶不存在。请检查存储桶名称和地域配置是否正确。原始错误：' . $errorMsg);
            } else {
                throw new Exception('分片上传失败: ' . $errorMsg);
            }
        }
    }
    
    /**
     * 初始化分片上传
     * @param string $key 对象键
     * @param string $contentType 内容类型
     * @return string uploadId
     * @throws Exception
     */
    protected function initiateMultipartUpload($key, $contentType = 'application/octet-stream')
    {
        $result = $this->cosClient->InitiateMultipartUpload([
            'Bucket' => $this->config['bucket'],
            'Key' => $key,
            'ContentType' => $contentType
        ]);
        
        return $result['UploadId'];
    }
    
    /**
     * 上传单个分片
     * @param string $filePath 本地文件路径
     * @param string $key 对象键
     * @param string $uploadId 上传ID
     * @param int $partNumber 分片号
     * @param int $offset 偏移量
     * @param int $partSize 分片大小
     * @return array 上传结果
     * @throws Exception
     */
    protected function uploadPart($filePath, $key, $uploadId, $partNumber, $offset, $partSize)
    {
        // 直接读取文件分片数据
        $fp = fopen($filePath, 'rb');
        fseek($fp, $offset);
        $content = fread($fp, $partSize);
        fclose($fp);
        
        $result = $this->cosClient->UploadPart([
            'Bucket' => $this->config['bucket'],
            'Key' => $key,
            'UploadId' => $uploadId,
            'PartNumber' => $partNumber,
            'Body' => $content
        ]);
        
        return $result;
    }
    
    /**
     * 完成分片上传
     * @param string $key 对象键
     * @param string $uploadId 上传ID
     * @param array $uploadParts 分片信息
     * @return array 上传结果
     * @throws Exception
     */
    protected function completeMultipartUpload($key, $uploadId, $uploadParts)
    {
        // 按分片号排序
        usort($uploadParts, function($a, $b) {
            return $a['PartNumber'] - $b['PartNumber'];
        });
        
        return $this->cosClient->CompleteMultipartUpload([
            'Bucket' => $this->config['bucket'],
            'Key' => $key,
            'UploadId' => $uploadId,
            'Parts' => $uploadParts
        ]);
    }
    
    /**
     * 终止分片上传
     * @param string $key 对象键
     * @param string $uploadId 上传ID
     * @throws Exception
     */
    protected function abortMultipartUpload($key, $uploadId)
    {
        $this->cosClient->AbortMultipartUpload([
            'Bucket' => $this->config['bucket'],
            'Key' => $key,
            'UploadId' => $uploadId
        ]);
    }
    
    /**
     * 生成对象键
     * @param string $filePath 本地文件路径
     * @return string
     */
    protected function generateKey($filePath)
    {
        $ext = pathinfo($filePath, PATHINFO_EXTENSION);
        $date = date('Ymd');
        $hash = Str::random(32);
        
        return "uploads/{$date}/{$hash}.{$ext}";
    }
    
    /**
     * 获取文件MIME类型
     * @param string $filePath 文件路径
     * @return string
     */
    protected function getContentType($filePath)
    {
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mimeType = finfo_file($finfo, $filePath);
        finfo_close($finfo);
        
        return $mimeType ?: 'application/octet-stream';
    }
    
    /**
     * 格式化上传结果
     * @param array $result COS返回结果
     * @param string $key 对象键
     * @return array 格式化后的结果
     */
    protected function formatResult($result, $key)
    {
        // 构建文件URL
        $protocol = 'https';
        $domain = $this->config['cdnDomain'] ?: "{$this->config['bucket']}.cos.{$this->config['region']}.myqcloud.com";
        $url = "{$protocol}://{$domain}/{$key}";
        
        return [
            'key' => $key,
            'url' => $url,
            'etag' => $result['ETag'] ?? '',
            'location' => $result['Location'] ?? $url,
            'request_id' => $result['RequestId'] ?? '',
            'success' => true
        ];
    }
    
    /**
     * 删除文件
     * @param string $key 对象键
     * @return bool
     * @throws Exception
     */
    public function delete($key)
    {
        try {
            $this->cosClient->deleteObject([
                'Bucket' => $this->config['bucket'],
                'Key' => $key
            ]);
            
            return true;
        } catch (Exception $e) {
            throw new Exception('删除文件失败: ' . $e->getMessage());
        }
    }
    
    /**
     * 批量删除文件
     * @param array $keys 对象键数组
     * @return bool
     * @throws Exception
     */
    public function batchDelete(array $keys)
    {
        if (empty($keys)) {
            return true;
        }
        
        try {
            // 构建删除请求
            $objects = array_map(function($key) {
                return ['Key' => $key];
            }, $keys);
            
            $this->cosClient->deleteObjects([
                'Bucket' => $this->config['bucket'],
                'Objects' => $objects
            ]);
            
            return true;
        } catch (Exception $e) {
            throw new Exception('批量删除失败: ' . $e->getMessage());
        }
    }
    
    /**
     * 获取文件信息
     * @param string $key 对象键
     * @return array
     * @throws Exception
     */
    public function getFileInfo($key)
    {
        try {
            $result = $this->cosClient->headObject([
                'Bucket' => $this->config['bucket'],
                'Key' => $key
            ]);
            
            return [
                'content_type' => $result['ContentType'],
                'content_length' => $result['ContentLength'],
                'last_modified' => $result['LastModified'],
                'etag' => $result['ETag'],
                'request_id' => $result['RequestId']
            ];
        } catch (Exception $e) {
            throw new Exception('获取文件信息失败: ' . $e->getMessage());
        }
    }
    
    /**
     * 检查文件是否存在
     * @param string $key 对象键
     * @return bool
     */
    public function doesObjectExist($key)
    {
        try {
            return $this->cosClient->doesObjectExist($this->config['bucket'], $key);
        } catch (Exception $e) {
            return false;
        }
    }
    
    /**
     * 生成预签名URL
     * @param string $key 对象键
     * @param int $expire 过期时间（秒）
     * @return string
     */
    public function getPresignedUrl($key, $expire = null)
    {
        $expire = $expire ?: $this->config['sign_expire'];
        $expires = time() + $expire;
        
        return $this->cosClient->getObjectUrl($this->config['bucket'], $key, $expires);
    }
    
    /**
     * 根据文件大小自动选择上传方式
     * @param string $filePath 本地文件路径
     * @param string $key 对象键
     * @param int $threshold 分片上传阈值(字节)
     * @param array $options 额外选项
     * @return array 上传结果
     * @throws Exception
     */
    public function autoUpload($filePath, $key = null, $threshold = 10485760, array $options = [])
    {
        // 检查文件是否存在
        if (!file_exists($filePath)) {
            throw new Exception('文件不存在');
        }
        
        $fileSize = filesize($filePath);
        
        // 如果文件大小超过阈值，使用分片上传
        if ($fileSize > $threshold) {
            return $this->uploadSlice($filePath, $key, $options);
        }
        
        // 否则使用简单上传
        return $this->upload($filePath, $key, $options);
    }
    
    /**
     * 保存文件信息到数据库
     * @param array $uploadResult 上传结果
     * @param array $fileInfo 文件信息
     * @param int $groupId 分组ID
     * @return array 保存结果
     */
    public function saveToDatabase($uploadResult, $fileInfo, $groupId = 0)
    {
        $data = [
            'admin_id' => 0,
            'group_id' => $groupId,
            'storage_path' => $uploadResult['key'],
            'url' => $uploadResult['url'],
            'hash' => $uploadResult['etag'] ?? Str::random(32),
            'origin_name' => $fileInfo['name'] ?? basename($fileInfo['path'] ?? ''),
            'object_name' => $uploadResult['key'],
            'media_type' => $fileInfo['type'] ?? 'file',
            'extension' => pathinfo($fileInfo['path'] ?? '', PATHINFO_EXTENSION),
            'mimetype' => $fileInfo['mime'] ?? 'application/octet-stream',
            'filesize' => $fileInfo['size'] ?? 0,
            'create_time' => time(),
            'update_time' => time()
        ];
        
        // 写入数据库
        $file = FileModel::create($data);
        
        return array_merge($data, ['id' => $file->id]);
    }
}